ドラッグして並べ替え

24 12月

text by kondo

ブラウザ上でのインターフェイスとして,何らかの要素を並べる仕組みが必要になりました。限られた要素から,一個ずつ選んでいくので,最初はドロップダウンリストを使おうかと考えたのですが(候補がだんだんと減っていく感じのもの),あとで修正ができるようにしたいと考えると,ドロップダウンリストでは具合が悪いです。結局,一直線に並べておいて,ドラッグして自由に順番を入れ替えることができるようなインターフェイスを目標としました。

最初は,jQueryのtouch-punchというプラグインを見つけて,その機能を利用しようとしました。これのsortableという機能が良さそうでした。下記はそのイメージ。

ただし,これは縦の並びで,横に書き換えたときには上手く働きません。そこで,touch-punchの機能のうち,ドラッグだけ利用して,自分で書いてみました。jQueryのプラグインという訳ではありません。下記がそのイメージとデモサイトへのリンクです。

サンプルへのリンク

 

(大まかな話を) ドラッグは位置決めだけに利用します。2枚のレイヤーを利用します。あるItem上にマウスが来たら,上のレイヤーにそのItemをコピーして,ドラッグ可能とします。これをドラッグして移動させて,適当なところでマウスから離したとき,そのマウスの位置を読み取って,元のItemを読み取った位置に移動させるという方法です。touch-punchで,一端ドラッグ可能とすると,このドラッグ機能を削除できませんでした。これが残っていると,配置が相対配置となって,自動的に左詰で整列してくれません。そこで,コピーしたDOMをドラッグ可能として移動させて,不要になったら削除するという手順となりました。

 

以下,順を追って書いてみます。まず,対象となるDOMを用意します。3つの div 要素と,並べ替える対象となる 7個の 小さな div 要素を準備します。下記のように改行してコードを整列させると,DOMとDOMの間に隙間が生じます。そこで,実際のソースでは改行を入れずに続けて書いています。詳しくは,このサイト(水平型リストで画像を並べた時の隙間を消す6つの方法)を参照してください。

 

 

<div id="newbox"></div>
<div id="partsbox">
<div class="round part" style="display: inline; z-index: 1;">Item 1</div>
<div class="round part" style="display: inline; z-index: 1;">Item 2</div>
<div class="round part" style="display: inline; z-index: 1;">Item 3</div>
<div class="round part" style="display: inline; z-index: 1;">Item 4</div>
<div class="round part" style="display: inline; z-index: 1;">Item 5</div>
<div class="round part" style="display: inline; z-index: 1;">Item 6</div>
<div class="round part" style="display: inline; z-index: 1;">Item 7</div>
</div>
<div id="enterbox"></div>

並べ替える対象の要素には,part というクラスをつけておきます。二つのdiv要素 partsbox と enterbox に,重ねて newbox を配置しています。下記のスタイルのソースをご覧ください。最初の配置では newbox が下にあります(z-index が 1)。

 

div#newbox {
position: absolute;
left: 0px;
top: 0px;
width: 630px;
height: 72px;
margin: 0px;
padding: 10px;
z-index: 1;
background-color: #ffffff;
border-width: 1px;
border-color: #ff0000;
border-style: solid;

opacity:0.8;
}

div#partsbox {
position: absolute;
left: 0px;
top: 0px;
width: 630px;
height: 22px;
margin: 0px;
padding: 10px;
z-index: 2;
background-color: #ffffff;
border-width: 1px;
border-color: #0000ff;
border-style: solid;
}

div#enterbox {
position: absolute;
left: 0px;
top: 50px;
width: 630px;
height: 22px;
margin: 0px;
padding: 10px;
z-index: 2;
background-color: #ffffff;
border-width: 1px;
border-color: #00ff00;
border-style: solid;
}

並べ替えの対象となる part というクラスがつけられたDOMのイベント(mouseenter)に,ハンドラーをバインドします。

$(function() {
$( '.part' ).each(function(){

$(this).bind("mouseenter",add_method);

});
});

 

下記は,関数 add_method の本体です(ログ出力のコードは省きました)。対象となるDOMの上にマウスが来たら,下記のコードを実行します。
まずはイベントの対象となったDOM要素を記録しておいて,その座標値を読み取ります(実際は多少座標値を修正)。次に対象DOM要素のクローンを作成して,newboxへコピーし,newboxのz-indexを3として最上位にします。コピーした要素にはdraggable()でドラッグ可能としておきます。
コピーされて,新しくnewboxに加えられたDOM要素のイベントには,さらに別のハンドラーを付け加えます。
ひとつは,mouseupです。まず,現在のマウス位置を読み取って,partsbox 上にあるか,enterbox上にあるかを判断します。その含まれている方のdivに,対象となったもともとのDOM要素をコピーします。コピーする際には,mouseupの位置から判断して,適当なDOM要素の間に挿入するようにします。
もうひとつは,mouseleaveです。これはnewboxにコピーしたDOMを消去して,newboxのz-indexを元の1に戻します。

 

function add_method(event){

dragged_item = $(event.target);

var x_position = dragged_item.offset().left - $('#container').offset().left -2;
var y_position = dragged_item.offset().top - $('#container').offset().top -2;

var an_element = dragged_item.clone().css({position:'absolute',left:x_position,top:y_position});

an_element.appendTo($('#newbox')).draggable();

an_element.mouseup(function(e){

$('#newbox').css('z-index','1');
flag_drop = 0;
var boxname = flag_enter(e.pageX,e.pageY);

if (boxname != 'out') {

var tmp_clone = dragged_item.clone();
var tmpid = document.getElementById(boxname);

$(tmpid).children().each(function() {
if (e.pageX < $(this).offset().left) {
$(this).before(tmp_clone);
tmp_clone.bind("mouseenter",add_method);
dragged_item.remove();
flag_drop = 1;
return false;
};
});

$(this).remove();

if (flag_drop == 0) {
tmp_clone.appendTo($(tmpid));
tmp_clone.bind("mouseenter",add_method);
dragged_item.remove();
}

} else if (boxname == 'out'){
$(this).remove();
};
});

an_element.mouseleave(function(event){
$(event.target).remove();
$('#newbox').css('z-index','1');
});

$('#newbox').css('z-index','3');

return false;
};

 

上記で,使用されていますが,マウスが,現在partsboxとenterboxのうち,どちらの上空にあるかを判断する関数は,下記のflag-enterです。それぞれのdivごとに,ゆっくり判断しています。mouseenterなどのイベントから判断せず,座標値から判断しています。

 

function flag_enter(x,y) {

var targetbox;
var rectangle_top;
var rectangle_left;
var rectangle_right;
var rectangle_bottom;
var flag = 0;

var which_box = 'out';

targetbox = $('#partsbox');

rectangle_top = targetbox.offset().top;
rectangle_bottom = Number(rectangle_top) + Number(targetbox.outerHeight());
rectangle_left = targetbox.offset().left;
rectangle_right = Number(rectangle_left) + Number(targetbox.outerWidth(true));

if ((rectangle_left < x) && (x < rectangle_right) && (rectangle_top < y) && (y < rectangle_bottom)) {
flag = 1;
which_box = 'partsbox';
} else {
flag = 0;
}

targetbox = $('#enterbox');

rectangle_top = targetbox.offset().top;
rectangle_bottom = Number(rectangle_top) + Number(targetbox.outerHeight());
rectangle_left = targetbox.offset().left;
rectangle_right = Number(rectangle_left) + Number(targetbox.outerWidth(true));

if ((rectangle_left < x) && (x < rectangle_right) && (rectangle_top < y) && (y < rectangle_bottom)) {
flag = 1;
which_box = 'enterbox';
} else {
flag = 0;
}

return which_box;
}

 

一応,iOSでもアンドロイドでも動いています。特に,デフォルトのタッチ機能を妨げることも無いようです。2本指での拡大縮小も機能します。